package net.finnigin.process;

import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class RunnableProcess implements Runnable {

    public static final int ABNORMAL_EXIT = Integer.MIN_VALUE;
    private static AtomicLong processCount = new AtomicLong();
    private final long pid;
    private ProcessState state;
    private Lock stateLock;
    protected ProcessBuilder processBuilder;
    protected Process process;
    protected int exitValue;
    protected Exception exception;
    protected boolean killed;
    private final LinkedList<ProcessListener> listeners;
    private static final ExecutorService notifier = new ProcessListenerNotifier();

    public RunnableProcess(List<String> command) {
        pid = processCount.getAndAdd(1L);
        processBuilder = new ProcessBuilder(command);
        stateLock = new ReentrantLock();
        killed = false;
        exception = null;
        state = ProcessState.NEW;
        listeners = new LinkedList<ProcessListener>();
    }

    @Override
    public void run() {
        try {
            setState(ProcessState.STARTING);
            beforeProcessStart();
            process = processBuilder.start();
            setState(ProcessState.STARTED);
            afterProcessStart();
            setState(ProcessState.RUNNING);
            exitValue = process.waitFor();
            setState(ProcessState.EXITING);
            afterProcessExit();
            setState(ProcessState.EXITED);
        } catch (Exception exception) {
            setState(ProcessState.FAILING);
            this.exception = exception;
            if (process != null) {
                process.destroy();
                exitValue = ABNORMAL_EXIT;
                killed = true;
            }
            caughtException();
            setState(ProcessState.FAILED);
        } finally {
            ProcessState oldState = state;
            setState(ProcessState.CLEANUP);
            doFinally();
            setState(oldState);
        }
    }

    protected void beforeProcessStart() throws Exception {
    }

    protected void afterProcessStart() throws Exception {
    }

    protected void afterProcessExit() throws Exception {
    }

    protected void caughtException() {
    }

    protected void doFinally() {
    }

    public long getPid() {
        return pid;
    }

    public List<String> getCommand() {
        return processBuilder.command();
    }

    public ProcessState getState() {
        return state;
    }

    public int exitValue() {
        return exitValue;
    }

    public Exception getException() {
        return exception;
    }

    public boolean isKilled() {
        return killed;
    }

    protected final void setState(ProcessState state) {
        stateLock.lock();
        try {
            this.state = state;
            this.fireProcessTransition(this.state);
        } finally {
            stateLock.unlock();
        }

    }

    public final void addProcessListener(ProcessListener p) {
        stateLock.lock();
        try {
            if (p == null) {
                return;
            }
            if (!listeners.contains(p)) {
                listeners.add(p);
            }
        } finally {
            stateLock.unlock();
        }

    }

    public final void removeProcessListener(ProcessListener p) {
        stateLock.lock();
        try {
            listeners.remove(p);
        } finally {
            stateLock.unlock();
        }
    }

    protected final void fireProcessTransition(ProcessState state) {
        stateLock.lock();
        try {
            for (ProcessListener l : listeners) {
                notifier.execute(new RunnableNotifier(l, new ProcessEvent(this, state)));
            }
        } finally {
            stateLock.unlock();
        }
    }

    private static final class ProcessListenerNotifier extends ThreadPoolExecutor {

        public ProcessListenerNotifier() {
            super(0, Integer.MAX_VALUE, 15L, TimeUnit.MINUTES, new SynchronousQueue<Runnable>(), new ThreadFactory() {

                private final ThreadFactory delegate = Executors.defaultThreadFactory();

                @Override
                public Thread newThread(Runnable r) {
                    Thread t = delegate.newThread(r);
                    t.setDaemon(true);
                    return t;
                }
            }, new ThreadPoolExecutor.DiscardPolicy());
        }
    }

    private final class RunnableNotifier implements Runnable {

        private final ProcessListener listener;
        private final ProcessEvent event;

        public RunnableNotifier(ProcessListener l, ProcessEvent e) {
            listener = l;
            event = e;
        }

        @Override
        public void run() {
            listener.processTransition(event);
        }
    }
}
